﻿package 
{
	import com.adobe.serialization.json.JSON;
	
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.net.URLLoader;
	
	import mx.containers.Canvas;
	import mx.core.UIComponent;

	public class TagCloud extends Canvas	{
		
		private var radius:Number;
		private var mcList:Array;
		private var dtr:Number;
		private var d:Number;
		private var sa:Number;
		private var ca:Number;
		private var sb:Number;
		private var cb:Number;
		private var sc:Number;
		private var cc:Number;
		private var originx:Number;
		private var originy:Number;
		private var tcolor:Number;
		private var hicolor:Number;
		private var tcolor2:Number;
		private var tspeed:Number;
		private var distr:Boolean;
		private var lasta:Number;
		private var lastb:Number;
		private var holder:UIComponent;
		private var active:Boolean;
		private var myXML:XML;
		private var myLoader:URLLoader;
		private var widthTag:Number;
		private var heightTag:Number;
		private var bInitialized:Boolean;
		
		//this number changes if it's elliptical or spherical
		[Bindable]
		public var howElliptical:Number=1;
		
		public function TagCloud(){		
			tspeed = 1;
			bInitialized=false;
		}
		
		/**
		 * Called from javascript with the definition of the tagcloud
		 * Before this is called the tagcloud is not rendered
		 */
		public function createTagCloud(strTags:String):void{
			trace("createTagCloud received JSON " + strTags);
			strTags=cleanJSONString(strTags);
			trace("createTagCloud after clean JSON " + strTags);
			var arTagCloud:Array = (JSON.decode(strTags) as Array);
			createCanvas(arTagCloud);
			bInitialized=true;
			
		}
		

		
		/**
		 * Called from javascript with the definition of the tagcloud
		 * This requires that the full JSON string of tags with count
		 */
		public function updateTagCloud(strTags:String):void{
			trace("updateTagCloud received JSON " + strTags);
			
			if(!bInitialized){
				createTagCloud(strTags);
				return;
			}
			strTags=cleanJSONString(strTags);
			var arTagCloud:Array = (JSON.decode(strTags) as Array);
			
			//hide all existing tags (might flicker)
			for each (var tag1:Tag in mcList )  {
				tag1.visible=false;
			}			
			
			//go through all received tags
			//if allready exist, change counter
			//if not create new
			for each (var newTag:Object in arTagCloud )  {
				var bExist:Boolean=false;
				for each (var tag:Tag in mcList )  {
					if(tag.tagName==newTag.name){
						tag.count=newTag.count;
						tag.visible=true;
						bExist=true;
					}
				}
				if (!bExist){
					var mc:Tag= createTagElement(newTag.name, newTag.count);
					mcList.push( mc );
					positionSingle(mc);
				}	
			}		
			//must resize all tags
			sizeAll();
			
		}
		
				
		/**
		 * Called from javascript when the tagcloud has changed
		 * Will lead to runtime change of the tagcloud
		 */	
		public function updateSingleTag(strTag:String):void{
			trace("received event " + strTag);
			var bExist:Boolean=false;
			
			for each (var tag:Tag in mcList )  {
				if(tag.tagName==strTag){
					tag.count=tag.count+1;
					bExist=true;
					break;
				}
			}
			
			if (!bExist){
				var mc:Tag= createTagElement(strTag, 1);
				mcList.push( mc );
				positionSingle(mc);
			}				
			//must resize all tags
			sizeAll();
		}
		
		private function createTagElement(strName:String, count:Number):Tag{	
				var col:Number = 0x000000;
				var hicol:Number = 0xFFEF07;
				var node:Object = new Object();
				node.name=strName;
				node.count=count;

				var mc:Tag = new Tag( node, col, hicol );
				holder.addChild(mc);
				holder.x = (this.widthTag/2) - (holder.width/2);
				holder.y = (this.heightTag/2) - (holder.height/2);
				return mc;
		}
		
		private function createCanvas(arTagCloud:Array):void {
			trace("Create canvas "+arTagCloud);
			
			//define sizes
			widthTag = this.width - this.width/10;
			heightTag = this.height - this.height/10;
			
			// set some vars
			radius = 150;
			dtr = Math.PI/180;
			d=300;
			sineCosine( 0,0,0 );
			mcList = [];
			active = false;
			lasta = 1;
			lastb = 1;
			// create holder mc, center it		
			holder = new UIComponent();
			addChild(holder);
			resizeHolder();
			// loop though them to find the smallest and largest 'tags'
			var largest:Number = 0;
			var smallest:Number = 9999;
			
			for each (var tag:Object in arTagCloud )  {
		        //largest = Math.max( largest, tag.meuPeso );
				//smallest = Math.min( smallest, tag.meuPeso );
		    
				// create movie clips					
				//var perc:Number = ( smallest == largest ) ? 1 : (tag.meuPeso-smallest) / (largest-smallest);
				var col:Number = 0x000000;
				var hicol:Number = 0xFFEF07;
				
				var mc:Tag = new Tag( tag, col, hicol );
				holder.addChild(mc);
				holder.x = (this.widthTag/2) - (holder.width/2);
				holder.y = (this.heightTag/2) - (holder.height/2);
				// store reference
				mcList.push( mc );
			}

			sizeAll();
			// distribute the tags on the sphere
			positionAll();
			// add event listeners
			this.addEventListener(Event.ENTER_FRAME, updateTags);
			parent.addEventListener(MouseEvent.ROLL_OUT, mouseExitHandler);
			parent.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
			this.addEventListener(Event.RESIZE, resizeHandler);

		}
		/**
		 * Adjusts the size of the tags based on the 
		 * count for each one 
		 * 
		 */
		private function sizeAll(): void {
			var largest:Number = 0;
			var smallest:Number = 9999;
			var sum:Number=0;
			var nrTags:Number=mcList.length;
			var avgSize:Number=16;
			
			var maxSize:Number=30;
			var minSize:Number=8;
			
			for each (var tag1:Tag in mcList )  {
				sum = sum + tag1.count ;
			}
			for each (var tag2:Tag in mcList )  {
				//var newSize:Number = Math.max((tag2.count/largest)*maxSize, minSize);
				var newSize:Number = (tag2.count/sum)*nrTags*avgSize ;
				trace(tag2.tagName +" c:" + tag2.count + " new size " + newSize);
				
				
				tag2.setSize(newSize);
			}
			
			
		}
		

		

		private function updateTags( e:Event ):void {
			var a:Number;
			var b:Number;
			if( active ){
				a = (-Math.min( Math.max( holder.mouseY, -250 ), 250 ) / 150 ) * tspeed;
				b = (Math.min( Math.max( holder.mouseX, -250 ), 250 ) /150 ) * tspeed;
			} else {
				a = lasta * 0.98;
				b = lastb * 0.98;
			}
			lasta = a;
			lastb = b;
			//trace("mousex: " + holder.mouseX + "mousey:" +holder.mouseY+ "a:" + a + " b:" + b);
			// if a and b under threshold, skip motion calculations to free up the processor
			if( Math.abs(a) > 0.01 || Math.abs(b) > 0.01 ){
				var c:Number = 0;
				sineCosine( a, b, c );
				// bewegen van de punten
				for( var j:Number=0; j<mcList.length; j++ ) {
					// multiply positions by a x-rotation matrix
					var rx1:Number = mcList[j].cx;
					var ry1:Number = mcList[j].cy * ca + mcList[j].cz * -sa;
					var rz1:Number = mcList[j].cy * sa + mcList[j].cz * ca;
					// multiply new positions by a y-rotation matrix
					var rx2:Number = rx1 * cb + rz1 * sb;
					var ry2:Number = ry1;
					var rz2:Number = rx1 * -sb + rz1 * cb;
					// multiply new positions by a z-rotation matrix
					var rx3:Number = rx2 * cc + ry2 * -sc;
					var ry3:Number = rx2 * sc + ry2 * cc;
					var rz3:Number = rz2;
					// set arrays to new positions
					mcList[j].cx = rx3;
					mcList[j].cy = ry3;
					mcList[j].cz = rz3;
					// add perspective
					var per:Number = d / (d+rz3);
					// setmc position, scale, alpha
					mcList[j].x = (howElliptical * rx3 * per) - (howElliptical*2); //original is mcList[j].x = rx3 * per;
					mcList[j].y = ry3 * per;
					mcList[j].scaleX = mcList[j].scaleY =  per;
					mcList[j].alpha = per/2;
				}
				depthSort();
			}
		}
		
		private function depthSort():void {
			mcList.sortOn( "cz", Array.DESCENDING | Array.NUMERIC );
			var current:Number = 0;
			for( var i:Number=0; i<mcList.length; i++ ){
				holder.setChildIndex( mcList[i], i );
				if( mcList[i].active == true ){
					current = i;
				}
			}
			holder.setChildIndex( mcList[current], mcList.length-1 );
		}
		
		/* See http://blog.massivecube.com/?p=9 */
		private function positionAll():void {		
			var phi:Number = 0;
			var theta:Number = 0;
			var max:Number = mcList.length;
			// mix up the list so not all a' live on the north pole
			mcList.sort( function():Number{ return Math.random()<0.5 ? 1 : -1; } );
			// distibute
			for( var i:Number=1; i<max+1; i++){
				if( distr ){
					phi = Math.acos(-1+(2*i-1)/max);
					theta = Math.sqrt(max*Math.PI)*phi;
				}else{
					phi = Math.random()*(Math.PI);
					theta = Math.random()*(2*Math.PI);
				}
				// Coordinate conversion
				mcList[i-1].cx = radius * Math.cos(theta)*Math.sin(phi);
				mcList[i-1].cy = radius * Math.sin(theta)*Math.sin(phi);
				mcList[i-1].cz = radius * Math.cos(phi);
			}
		}
		
		private function positionSingle (tag:Tag):void{
				var phi:Number = 0;
				var theta:Number = 0;
				phi = Math.random()*(Math.PI);
				theta = Math.random()*(2*Math.PI);

				tag.cx = radius * Math.cos(theta)*Math.sin(phi);
				tag.cy = radius * Math.sin(theta)*Math.sin(phi);
				tag.cz = radius * Math.cos(phi);
		}
		
	
		private function mouseExitHandler( e:Event ):void {
			 active = false; 
			//trace("mouseExitHandler, active: " + active);
		}
		private function mouseMoveHandler( e:MouseEvent ):void {
			active = true;
			//trace("mouseMoveHandler, active: " + active);
		}
		private function resizeHandler( e:Event ):void { resizeHolder(); }
		
		private function resizeHolder():void {
			
			holder.x = (this.width/2) - (holder.width/2);
			holder.y = (this.height/2) - (holder.height/2);
			var scale:Number;
			if( width > height ){
				scale = (height/500);
			} else {
				scale = (width/500);
			}
			holder.scaleX = holder.scaleY = scale;
		}
		
		private function sineCosine( a:Number, b:Number, c:Number ):void {
			sa = Math.sin(a * dtr);
			ca = Math.cos(a * dtr);
			sb = Math.sin(b * dtr);
			cb = Math.cos(b * dtr);
			sc = Math.sin(c * dtr);
			cc = Math.cos(c * dtr);
		}
		
		
		private function getColorFromGradient( perc:Number ):Number {
			var r:Number = ( perc * ( tcolor >> 16 ) ) + ( (1-perc) * ( tcolor2 >> 16 ) );
			var g:Number = ( perc * ( (tcolor >> 8) % 256 ) ) + ( (1-perc) * ( (tcolor2 >> 8) % 256 ) );
			var b:Number = ( perc * ( tcolor % 256 ) ) + ( (1-perc) * ( tcolor2 % 256 ) );
			return( (r << 16) | (g << 8) | b );
		}
		
		public function cleanJSONString(str:String):String{ 
			var pattern:RegExp = /\'/g;
			//'(fix editor problem)
			return str.replace(pattern,"\"");
		}
		
	}

}